เจาะลึก React Batched Updates วิธีการปรับปรุงประสิทธิภาพโดยลดการ re-render ที่ไม่จำเป็น และแนวทางปฏิบัติที่ดีที่สุดเพื่อใช้งานอย่างมีประสิทธิภาพ
React Batched Updates: การปรับปรุงประสิทธิภาพการเปลี่ยนแปลง State
ประสิทธิภาพของ React เป็นสิ่งสำคัญอย่างยิ่งในการสร้าง User Interface ที่ราบรื่นและตอบสนองได้ดี หนึ่งในกลไกสำคัญที่ React ใช้เพื่อปรับปรุงประสิทธิภาพคือ batched updates เทคนิคนี้จะรวมการอัปเดต state หลายๆ ครั้งไว้ในการ re-render เพียงรอบเดียว ซึ่งช่วยลดจำนวนการ re-render ที่ไม่จำเป็นลงอย่างมาก และปรับปรุงการตอบสนองโดยรวมของแอปพลิเคชัน บทความนี้จะเจาะลึกถึงความซับซ้อนของ batched updates ใน React โดยอธิบายวิธีการทำงาน ประโยชน์ ข้อจำกัด และวิธีนำไปใช้อย่างมีประสิทธิภาพเพื่อสร้างแอปพลิเคชัน React ที่มีประสิทธิภาพสูง
ทำความเข้าใจกระบวนการ Rendering ของ React
ก่อนที่จะเจาะลึกเรื่อง batched updates สิ่งสำคัญคือต้องเข้าใจกระบวนการ rendering ของ React ก่อน เมื่อใดก็ตามที่ state ของ component เปลี่ยนแปลง React จะต้อง re-render component นั้นและ component ลูกๆ เพื่อแสดง state ใหม่ใน User Interface กระบวนการนี้ประกอบด้วยขั้นตอนต่อไปนี้:
- การอัปเดต State: State ของ component จะถูกอัปเดตโดยใช้เมธอด
setState(หรือ hook อย่างuseState) - Reconciliation: React จะเปรียบเทียบ virtual DOM ใหม่กับของเดิมเพื่อหาความแตกต่าง (the "diff")
- Commit: React จะอัปเดต DOM จริงตามความแตกต่างที่พบ นี่คือขั้นตอนที่การเปลี่ยนแปลงจะปรากฏให้ผู้ใช้เห็น
การ re-render อาจเป็นกระบวนการที่ใช้ทรัพยากรการคำนวณสูง โดยเฉพาะสำหรับ component ที่ซับซ้อนและมี component tree ที่ลึก การ re-render บ่อยครั้งอาจนำไปสู่ปัญหาคอขวดด้านประสิทธิภาพและประสบการณ์ผู้ใช้ที่ช้าลงได้
Batched Updates คืออะไร?
Batched updates เป็นเทคนิคการปรับปรุงประสิทธิภาพที่ React ใช้ในการรวมการอัปเดต state หลายๆ ครั้งไว้ในรอบการ re-render เพียงครั้งเดียว แทนที่จะ re-render component ทุกครั้งหลังจากการเปลี่ยนแปลง state แต่ละครั้ง React จะรอจนกว่าการอัปเดต state ทั้งหมดภายในขอบเขตที่กำหนดจะเสร็จสิ้น แล้วจึงทำการ re-render เพียงครั้งเดียว ซึ่งช่วยลดจำนวนครั้งที่ DOM ถูกอัปเดตลงอย่างมาก นำไปสู่ประสิทธิภาพที่ดีขึ้น
Batched Updates ทำงานอย่างไร
React จะทำการ batch การอัปเดต state โดยอัตโนมัติที่เกิดขึ้นภายในสภาพแวดล้อมที่ควบคุมได้ เช่น:
- Event handlers: การอัปเดต state ภายใน event handlers เช่น
onClick,onChangeและonSubmitจะถูก batch - React Lifecycle Methods (Class Components): การอัปเดต state ภายใน lifecycle methods เช่น
componentDidMountและcomponentDidUpdateก็จะถูก batch เช่นกัน - React Hooks: การอัปเดต state ที่ทำผ่าน
useStateหรือ custom hooks ที่ถูกเรียกโดย event handlers จะถูก batch
เมื่อมีการอัปเดต state หลายครั้งเกิดขึ้นในบริบทเหล่านี้ React จะนำไปเข้าคิว แล้วจึงดำเนินการ reconciliation และ commit เพียงครั้งเดียวหลังจากที่ event handler หรือ lifecycle method ทำงานเสร็จสิ้น
ตัวอย่าง:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
return (
Count: {count}
);
}
export default Counter;
ในตัวอย่างนี้ การคลิกปุ่ม "Increment" จะเรียกฟังก์ชัน handleClick ซึ่งเรียก setCount สามครั้ง React จะรวมการอัปเดต state ทั้งสามครั้งนี้ไว้ในการอัปเดตครั้งเดียว ผลลัพธ์คือ component จะ re-render เพียงครั้งเดียว และค่า count จะเพิ่มขึ้น 3 ไม่ใช่ 1 สำหรับการเรียก setCount แต่ละครั้ง หาก React ไม่ได้ทำการ batch updates, component จะ re-render ถึงสามครั้ง ซึ่งมีประสิทธิภาพน้อยกว่า
ประโยชน์ของ Batched Updates
ประโยชน์หลักของ batched updates คือการปรับปรุงประสิทธิภาพโดยการลดจำนวนการ re-render ซึ่งนำไปสู่:
- การอัปเดต UI ที่เร็วขึ้น: การ re-render ที่ลดลงส่งผลให้การอัปเดต User Interface รวดเร็วยิ่งขึ้น ทำให้แอปพลิเคชันตอบสนองได้ดีขึ้น
- การจัดการ DOM ที่ลดลง: การอัปเดต DOM ที่ไม่บ่อยครั้งหมายถึงภาระงานของเบราว์เซอร์ที่น้อยลง นำไปสู่ประสิทธิภาพที่ดีขึ้นและการใช้ทรัพยากรที่ต่ำลง
- ประสิทธิภาพโดยรวมของแอปพลิเคชันที่ดีขึ้น: Batched updates ช่วยให้ประสบการณ์ผู้ใช้ราบรื่นและมีประสิทธิภาพมากขึ้น โดยเฉพาะในแอปพลิเคชันที่ซับซ้อนซึ่งมีการเปลี่ยนแปลง state บ่อยครั้ง
กรณีที่ Batched Updates ไม่ทำงาน
แม้ว่า React จะทำการ batch updates โดยอัตโนมัติในหลายสถานการณ์ แต่ก็มีบางสถานการณ์ที่การ batching จะไม่เกิดขึ้น:
- การทำงานแบบ Asynchronous (นอกเหนือการควบคุมของ React): การอัปเดต state ที่เกิดขึ้นภายใน asynchronous operations เช่น
setTimeout,setIntervalหรือ promises โดยทั่วไปแล้วจะไม่ถูก batch โดยอัตโนมัติ เนื่องจาก React ไม่สามารถควบคุมบริบทการทำงานของ operations เหล่านี้ได้ - Native Event Handlers: หากคุณใช้ native event listeners (เช่น การผูก listener กับ DOM elements โดยตรงด้วย
addEventListener) การอัปเดต state ภายใน handlers เหล่านั้นจะไม่ถูก batch
ตัวอย่าง (Asynchronous Operation):
import React, { useState } from 'react';
function DelayedCounter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}, 0);
};
return (
Count: {count}
);
}
export default DelayedCounter;
ในตัวอย่างนี้ แม้ว่า setCount จะถูกเรียกสามครั้งติดต่อกัน แต่เนื่องจากอยู่ภายใน callback ของ setTimeout ผลลัพธ์คือ React จะ *ไม่* batch การอัปเดตเหล่านี้ และ component จะ re-render สามครั้ง โดยเพิ่มค่า count ทีละ 1 ในแต่ละการ re-render พฤติกรรมนี้เป็นสิ่งสำคัญที่ต้องทำความเข้าใจเพื่อการปรับปรุงประสิทธิภาพ component ของคุณอย่างเหมาะสม
การบังคับให้เกิด Batch Updates ด้วย `unstable_batchedUpdates`
ในสถานการณ์ที่ React ไม่ทำการ batch updates โดยอัตโนมัติ คุณสามารถใช้ unstable_batchedUpdates จาก react-dom เพื่อบังคับให้เกิดการ batching ได้ ฟังก์ชันนี้ช่วยให้คุณสามารถครอบการอัปเดต state หลายๆ ครั้งไว้ใน batch เดียว ทำให้มั่นใจได้ว่าจะถูกประมวลผลพร้อมกันในรอบการ re-render เพียงครั้งเดียว
หมายเหตุ: API unstable_batchedUpdates ถือว่าไม่เสถียรและอาจมีการเปลี่ยนแปลงใน React เวอร์ชันอนาคต ควรใช้อย่างระมัดระวังและเตรียมพร้อมที่จะปรับแก้โค้ดของคุณหากจำเป็น อย่างไรก็ตาม มันยังคงเป็นเครื่องมือที่มีประโยชน์สำหรับการควบคุมพฤติกรรมการ batching อย่างชัดเจน
ตัวอย่าง (การใช้ `unstable_batchedUpdates`):
import React, { useState } from 'react';
import { unstable_batchedUpdates } from 'react-dom';
function DelayedCounter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
unstable_batchedUpdates(() => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
});
}, 0);
};
return (
Count: {count}
);
}
export default DelayedCounter;
ในตัวอย่างที่แก้ไขนี้ unstable_batchedUpdates ถูกใช้เพื่อครอบการเรียก setCount ทั้งสามครั้งที่อยู่ภายใน callback ของ setTimeout ซึ่งเป็นการบังคับให้ React ทำการ batch การอัปเดตเหล่านี้ ส่งผลให้เกิดการ re-render เพียงครั้งเดียวและเพิ่มค่า count ขึ้น 3
React 18 และ Automatic Batching
React 18 ได้นำเสนอ automatic batching สำหรับสถานการณ์ที่หลากหลายมากขึ้น ซึ่งหมายความว่า React จะทำการ batch การอัปเดต state โดยอัตโนมัติ แม้ว่าจะเกิดขึ้นภายใน timeouts, promises, native event handlers หรือ event อื่นๆ ก็ตาม สิ่งนี้ช่วยให้การปรับปรุงประสิทธิภาพง่ายขึ้นอย่างมากและลดความจำเป็นในการใช้ unstable_batchedUpdates ด้วยตนเอง
ตัวอย่าง (React 18 Automatic Batching):
import React, { useState } from 'react';
function DelayedCounter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}, 0);
};
return (
Count: {count}
);
}
export default DelayedCounter;
ใน React 18 ตัวอย่างข้างต้นจะทำการ batch การเรียก setCount โดยอัตโนมัติ แม้ว่าจะอยู่ภายใน setTimeout ก็ตาม นี่คือการปรับปรุงที่สำคัญในความสามารถด้านการปรับปรุงประสิทธิภาพของ React
แนวทางปฏิบัติที่ดีที่สุดสำหรับการใช้ Batched Updates
เพื่อที่จะใช้ประโยชน์จาก batched updates และปรับปรุงประสิทธิภาพแอปพลิเคชัน React ของคุณอย่างมีประสิทธิภาพ ลองพิจารณาแนวทางปฏิบัติที่ดีที่สุดต่อไปนี้:
- รวมการอัปเดต State ที่เกี่ยวข้องกัน: เมื่อใดก็ตามที่เป็นไปได้ ให้รวมการอัปเดต state ที่เกี่ยวข้องกันไว้ใน event handler หรือ lifecycle method เดียวกันเพื่อเพิ่มประโยชน์สูงสุดจากการ batching
- หลีกเลี่ยงการอัปเดต State ที่ไม่จำเป็น: ลดจำนวนการอัปเดต state ให้น้อยที่สุดโดยการออกแบบ state ของ component อย่างรอบคอบ และหลีกเลี่ยงการอัปเดตที่ไม่จำเป็นซึ่งไม่ส่งผลกระทบต่อ User Interface พิจารณาใช้เทคนิคเช่น memoization (เช่น
React.memo) เพื่อป้องกันการ re-render ของ component ที่ props ไม่ได้เปลี่ยนแปลง - ใช้ Functional Updates: เมื่อต้องการอัปเดต state โดยอ้างอิงจาก state ก่อนหน้า ให้ใช้ functional updates วิธีนี้ช่วยให้มั่นใจได้ว่าคุณกำลังทำงานกับค่า state ที่ถูกต้อง แม้ว่าการอัปเดตจะถูก batch ก็ตาม Functional updates คือการส่งฟังก์ชันไปยัง
setState(หรือ setter ของuseState) ซึ่งจะได้รับ state ก่อนหน้าเป็นอาร์กิวเมนต์ - ระมัดระวังเรื่อง Asynchronous Operations: ใน React เวอร์ชันเก่า (ก่อน 18) โปรดทราบว่าการอัปเดต state ภายใน asynchronous operations จะไม่ถูก batch โดยอัตโนมัติ ให้ใช้
unstable_batchedUpdatesเมื่อจำเป็นเพื่อบังคับการ batching อย่างไรก็ตาม สำหรับโปรเจกต์ใหม่ ขอแนะนำอย่างยิ่งให้อัปเกรดเป็น React 18 เพื่อใช้ประโยชน์จาก automatic batching - ปรับปรุง Event Handlers: ปรับปรุงโค้ดภายใน event handlers ของคุณเพื่อหลีกเลี่ยงการคำนวณที่ไม่จำเป็นหรือการจัดการ DOM ที่อาจทำให้กระบวนการ rendering ช้าลง
- ทำโปรไฟล์แอปพลิเคชันของคุณ: ใช้เครื่องมือ profiling ของ React เพื่อระบุปัญหาคอขวดด้านประสิทธิภาพและส่วนที่สามารถปรับปรุง batched updates เพิ่มเติมได้ แท็บ Performance ใน React DevTools สามารถช่วยให้คุณเห็นภาพการ re-render และหาโอกาสในการปรับปรุงได้
ตัวอย่าง (Functional Updates):
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);
};
return (
Count: {count}
);
}
export default Counter;
ในตัวอย่างนี้ functional updates ถูกใช้เพื่อเพิ่มค่า count โดยอ้างอิงจากค่าก่อนหน้า ซึ่งช่วยให้มั่นใจได้ว่าค่า count จะถูกเพิ่มอย่างถูกต้อง แม้ว่าการอัปเดตจะถูก batch ก็ตาม
สรุป
Batched updates ของ React เป็นกลไกที่ทรงพลังสำหรับการปรับปรุงประสิทธิภาพโดยการลดการ re-render ที่ไม่จำเป็น การทำความเข้าใจวิธีการทำงาน ข้อจำกัด และวิธีใช้ประโยชน์อย่างมีประสิทธิภาพเป็นสิ่งสำคัญสำหรับการสร้างแอปพลิเคชัน React ที่มีประสิทธิภาพสูง ด้วยการปฏิบัติตามแนวทางที่ดีที่สุดที่ระบุไว้ในบทความนี้ คุณสามารถปรับปรุงการตอบสนองและประสบการณ์ผู้ใช้โดยรวมของแอปพลิเคชัน React ของคุณได้อย่างมาก และด้วยการที่ React 18 นำเสนอ automatic batching การปรับปรุงการเปลี่ยนแปลง state ก็ยิ่งง่ายและมีประสิทธิภาพมากขึ้น ทำให้นักพัฒนาสามารถมุ่งเน้นไปที่การสร้าง User Interface ที่ยอดเยี่ยมได้